// --------------------------------------------------------------------------------------------------------------------
// <copyright file="BasicUnitTest.cs" company="Microsoft Corporation">
// The MIT License (MIT)
// 
// Copyright (c) 2014, Microsoft Corporation
// 
// Permission is hereby granted, free of charge, to any person obtaining a copy
//  of this software and associated documentation files (the "Software"), to deal
//  in the Software without restriction, including without limitation the rights
//  to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
//  copies of the Software, and to permit persons to whom the Software is
//  furnished to do so, subject to the following conditions:
// 
// The above copyright notice and this permission notice shall be included in
//  all copies or substantial portions of the Software.
// 
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
//  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
//  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
//  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
//  THE SOFTWARE.
// </copyright>
// --------------------------------------------------------------------------------------------------------------------
namespace Test.Robotics.Runtime
{
    using System;
    using System.IO;
    using System.Reflection;
    using System.Runtime.InteropServices;
    using System.Runtime.Serialization;
    using System.Threading;
    using Microsoft.Robotics.IO;
    using Microsoft.Robotics.Runtime;
    using Microsoft.VisualStudio.TestTools.UnitTesting;

    /// <summary>
    /// A struct that should be serialized in packed form
    /// </summary>
    [StructLayout(LayoutKind.Sequential)]
    public struct SerializableStruct
    {
        /// <summary>
        /// First field
        /// </summary>
        public int A;

        /// <summary>
        /// Second field
        /// </summary>
        public int B;
    }

    /////// <summary>
    /////// A struct that should not be serialized because it's missing the packing annotation
    /////// </summary>
    ////struct NonserializableStruct
    ////{
    ////    public int A;
    ////    public int B;
    ////    public int C;
    ////}

    /// <summary>
    /// Basic set of unit tests for the Agent Runtime.
    /// </summary>
    [TestClass]
    public class BasicUnitTest
    {
        /// <summary>
        /// Validate that agents can be created and registered successfully
        /// </summary>
        [TestMethod]
        [TestCategory("Unit")]
        [Priority(0)]
        public void AgentCreationTest()
        {
            TimerAgent agent = new TimerAgent("test", 30);
            AgentLocator locator = new AgentLocator();
            IAgentHost host = locator.Register(agent);

            // no exception so far means we successfully created the agent
        }

        /// <summary>
        /// Validates that a downstream agent can activate/deactivate an upstream agent it subscribes to
        /// </summary>
        [TestMethod]
        [TestCategory("Unit")]
        [Priority(0)]
        public void AgentActivationTest()
        {
            AgentLocator locator = new AgentLocator();
            TimerAgent agent = new TimerAgent("test", 30);
            IAgentHost timerHost = locator.Register(agent);

            SimpleSubscriberAgent subscriberAgent = new SimpleSubscriberAgent("subscriber", agent);
            IAgentHost subscriberHost = locator.Register(subscriberAgent);

            // calling Activate on the subscriber should result in a cascading activation of the timer
            subscriberHost.Activate();
            if (!timerHost.IsActive)
            {
                throw new Exception("Upstream agent was not activated.");
            }

            subscriberHost.Deactivate();
            if (timerHost.IsActive)
            {
                throw new Exception("Upstream agent was not deactivated.");
            }
        }

        /// <summary>
        /// Validates that calling deactivate more than once on an agent will cause an exception
        /// </summary>
        [TestMethod, ExpectedException(typeof(InvalidOperationException))]
        [TestCategory("Unit")]
        [Priority(0)]
        public void AgentDeactivateExceptionTest()
        {
            IAgentHost subscriberHost = null;

            try
            {
                AgentLocator locator = new AgentLocator();
                TimerAgent agent = new TimerAgent("test", 30);
                locator.Register(agent);

                SimpleSubscriberAgent subscriberAgent = new SimpleSubscriberAgent("subscriber", agent);
                subscriberHost = locator.Register(subscriberAgent);

                subscriberHost.Activate();
                subscriberHost.Deactivate();
            }
            catch (Exception e)
            {
                // We want to make sure that if any of calls above throws InvalidOperationException
                // which they may for various reasons - we dont confuse those exceptions with the one
                // that deactivate must throw
                throw new Exception("Unexpected exception thrown", e);
            }

            subscriberHost.Deactivate(); // this should throw
        }

        /// <summary>
        /// Validates that the agent host correctly discovers multiple interfaces and turns them into connection points
        /// </summary>
        [Priority(0)]
        [TestMethod]
        [TestCategory("Unit")]
        public void AgentMetadataDiscovery()
        {
            AgentLocator locator = new AgentLocator();
            EchoAgent echoAgent = new EchoAgent("EchoAgent");
            IAgentHost echoHost = locator.Register(echoAgent);
            echoHost.Activate();

            DoubleSubscriptionAgent subscriptionAgent = new DoubleSubscriptionAgent("SubscriptionAgent");
            IAgentHost subscriptionHost = locator.Register(subscriptionAgent);
            subscriptionHost.Activate();

            // post control messages, which in turn will cause the echo agent to publish
            IForwarder<SimpleAgentMessage<int>> intControl = echoHost.GetControlPort<SimpleAgentMessage<int>>();
            IForwarder<SimpleAgentMessage<double>> doubleControl = echoHost.GetControlPort<SimpleAgentMessage<double>>();
            intControl.Post(new SimpleAgentMessage<int>(1, AgentMessage.GetCurrentTime()));
            doubleControl.Post(new SimpleAgentMessage<double>(2, AgentMessage.GetCurrentTime()));

            // check that the messages were delivered to the echo agent and published downstream via the rights methods
            if (!echoAgent.Wait(250))
            {
                throw new Exception("Not all methods were called on echoAgent");
            }

            // check that the messages published by the echo agent made it to the subscription agent via the correct methods
            if (!subscriptionAgent.AllMethodsCalledEvent.WaitOne(250))
            {
                throw new Exception("Not all methods were called on subscriptionAgent");
            }

            subscriptionHost.Deactivate();
            echoHost.Deactivate();
        }

        /// <summary>
        /// Verifies that agent methods are delivered in sequence.
        /// Uses a fast publisher to send messages to a slower subscriber, to validate that the messages are delivered in sequence.
        /// Also validates that the default forwarder/receiver correctly returns the last posted message.
        /// </summary>
        [Priority(0)]
        [TestMethod]
        [TestCategory("Unit")]
        public void AgentMessageOrdering()
        {
            AgentLocator locator = new AgentLocator();
            int count = 10;

            var producer = new MessageInjectorAgent<SimpleAgentMessage<int>>("producer");
            IAgentHost producerHost = locator.Register(producer);
            var consumer = new MessageCollectorAgent<SimpleAgentMessage<int>>("consumer", producer, count);
            IAgentHost consumerHost = locator.Register(consumer);

            // start both
            // activate the producer explicitly so that it remains active even after the consumer is deactivated
            producerHost.Activate();
            consumerHost.Activate();

            // pump messages as fast as we can
            int i = 0;
            while (!consumer.Wait(10))
            {
                for (int j = 0; j < 100; j++)
                {
                    producer.Publish(new SimpleAgentMessage<int>(i++, AgentMessage.GetCurrentTime()));
                }
            }

            // deactivate the consumer and pump some more
            consumerHost.Deactivate();
            for (int j = 0; j < 100; j++)
            {
                producer.Publish(new SimpleAgentMessage<int>(i++, AgentMessage.GetCurrentTime()));
            }

            // activate the consumer again, and post more messages
            consumerHost.Activate();
            while (!consumer.Wait(10))
            {
                for (int j = 0; j < 100; j++)
                {
                    producer.Publish(new SimpleAgentMessage<int>(i++, AgentMessage.GetCurrentTime()));
                }
            }

            // shutdown first, to ensure message flow is stopped and we can look at the resutls collection
            consumerHost.Deactivate();
            producerHost.Deactivate();

            // all messages should be ordered
            int previous = -1;
            foreach (SimpleAgentMessage<int> m in consumer.Results)
            {
                Assert.IsTrue(previous < m.State);
                previous = m.State;
            }
        }

        /// <summary>
        /// Verifies that an inactive agent doesn't receive messages
        /// </summary>
        [Priority(0)]
        [TestMethod]
        [TestCategory("Unit")]
        public void AgentMessageConsumptionOnActivation()
        {
            AgentLocator locator = new AgentLocator();
            var producer = new MessageInjectorAgent<SimpleAgentMessage<int>>("producer");
            IAgentHost producerHost = locator.Register(producer);
            var consumer = new MessageCollectorAgent<SimpleAgentMessage<int>>("consumer", producer, 1);
            IAgentHost consumerHost = locator.Register(consumer);

            // now activate the producer
            producerHost.Activate();

            // loop twice, so we cover the initial state and the state after one deactivation
            for (int i = 0; i < 2; i++)
            {
                // publish a message and check that the consumer is not receiving it
                producer.Publish(new SimpleAgentMessage<int>(0, AgentMessage.GetCurrentTime()));
                if (consumer.Wait(100))
                {
                    Assert.Fail("Consumer was not active but still received the message?");
                }

                // activate the consumer
                consumerHost.Activate();
                producer.Publish(new SimpleAgentMessage<int>(1, AgentMessage.GetCurrentTime()));

                // wait for the consumer and verify it received only the second message
                if (!consumer.Wait(1000))
                {
                    throw new Exception("Consumer agent did not wake up. Check the test output window for any exceptions.");
                }

                // check the message
                Assert.AreEqual(1, consumer.Results.Count);
                Assert.AreEqual(1, consumer.Results.Dequeue().State);

                // deactivate the consumer 
                consumerHost.Deactivate();
            }

            // shutdown
            producerHost.Deactivate();
        }

        /// <summary>
        /// Validates that an agent deactivates correctly even if the agent to which it subscribes publishes messages very fast
        /// </summary>
        [TestMethod]
        [TestCategory("Unit")]
        [Priority(0)]
        public void TestDeactivationUnderPressure()
        {
            // create the pipeline in offline mode
            AgentLocator locator = new AgentLocator(true);
            AutoResetEvent started = new AutoResetEvent(false);
            int count = 0;

            // create a fast producer that never stops
            SimpleProducerAgent producer = new SimpleProducerAgent("producer", 10000, 1);
            locator.Register(producer);

            // create a consumer that works much slower than the producer. It's queue is always going to be full.
            DelegateAgent<SimpleAgentMessage<int>> subscriber = new DelegateAgent<SimpleAgentMessage<int>>(
                "subscriber",
                producer,
                m =>
                {
                    Thread.Sleep(50);
                    started.Set();
                    count++;
                });
            IAgentHost subscriberHost = locator.Register(subscriber);

            // start both
            subscriberHost.Activate();
            if (!started.WaitOne(200))
            {
                throw new Exception("wait timed out");
            }

            subscriberHost.Deactivate();

            // we expect the subscriber to have executed a small number of times (about 2, definitely less than 10)
            if (count > 10)
            {
                throw new Exception("Deactivation failed to stop the producer agent");
            }
        }

        /// <summary>
        /// Validates that no messages get dropped in offline mode
        /// </summary>
        [TestMethod]
        [TestCategory("Unit")]
        [Priority(0)]
        public void OfflineModeDoesntDropMessagesTest()
        {
            int value = 0;
            AutoResetEvent completionEvent = new AutoResetEvent(false);

            // create the pipeline in offline mode
            AgentLocator locator = new AgentLocator(true);

            // create a producer - this one will pump 6 messages at 1ms intervals
            SimpleProducerAgent producer = new SimpleProducerAgent("producer", 6, 1);
            IAgentHost producerHost = locator.Register(producer);

            // create a consumer that works much slower than the producer
            DelegateAgent<SimpleAgentMessage<int>> subscriber = new DelegateAgent<SimpleAgentMessage<int>>(
                "subscriber",
                producer,
                m =>
                {
                    value = value + m.State;
                    Thread.Sleep(20);
                });
            IAgentHost subscriberHost = locator.Register(subscriber);

            // start both
            subscriberHost.Activate();

            // wait for the producer to finish publishing
            if (!producer.Wait(1000))
            {
                throw new Exception("Wait timed out");
            }

            subscriberHost.Deactivate();

            // the value should now be either 10 or 15 (the last message might not have made it before we deactivated the subscriber)
            if (value != 10 && value != 15)
            {
                throw new Exception("The end state is not what was expected. Value = " + value);
            }
        }

        /// <summary>
        /// Validates the SerializableArray and SerializationSurrogate for uncompressed arrays.
        /// </summary>
        [TestMethod]
        [TestCategory("Unit")]
        [Priority(0)]
        public void AgentSerializableArrayUncompressedTest()
        {
            this.CheckUncompressedSerializableArray<double>(sizeof(double), 0xEF);
            this.CheckUncompressedSerializableArray<bool>(sizeof(bool), true);
            this.CheckUncompressedSerializableArray<char>(sizeof(char), 'A');

            this.CheckUncompressedSerializableArray<short>(sizeof(short), 0xEF);
            this.CheckUncompressedSerializableArray<int>(sizeof(int), 0xEF);
            this.CheckUncompressedSerializableArray<long>(sizeof(long), 0xEF);
            this.CheckUncompressedSerializableArray<float>(sizeof(float), 0xEF);
            this.CheckUncompressedSerializableArray<ushort>(sizeof(ushort), 0xEF);
            this.CheckUncompressedSerializableArray<uint>(sizeof(uint), 0xEF);
            this.CheckUncompressedSerializableArray<ulong>(sizeof(ulong), 0xEF);
            this.CheckUncompressedSerializableArray<SerializableStruct>(2 * sizeof(int), new SerializableStruct() { A = 0x11, B = 0xFF });
        }

        /// <summary>
        /// Validates the SerializableArray and SerializationSurrogate for compressed Arrays.
        /// </summary>
        [TestMethod]
        [TestCategory("Unit")]
        [Priority(0)]
        public void AgentSerializableArrayCompressedTest()
        {
            this.CheckCompressedSerializableArray<double>(sizeof(double), 0xEF);
            this.CheckCompressedSerializableArray<bool>(sizeof(bool), true);
            this.CheckCompressedSerializableArray<char>(sizeof(char), 'A');

            this.CheckCompressedSerializableArray<short>(sizeof(short), 0xEF);
            this.CheckCompressedSerializableArray<int>(sizeof(int), 0xEF);
            this.CheckCompressedSerializableArray<long>(sizeof(long), 0xEF);
            this.CheckCompressedSerializableArray<float>(sizeof(float), 0xEF);
            this.CheckCompressedSerializableArray<ushort>(sizeof(ushort), 0xEF);
            this.CheckCompressedSerializableArray<uint>(sizeof(uint), 0xEF);
            this.CheckCompressedSerializableArray<ulong>(sizeof(ulong), 0xEF);
            this.CheckCompressedSerializableArray<SerializableStruct>(2 * sizeof(int), new SerializableStruct() { A = 0x11, B = 0xFF });
        }

        /// <summary>
        /// Validates the SerializableArray and SerializationSurrogate for small Arrays, which should remain uncompressed even if compression is specified.
        /// Regression test for TFS 1669 Memory stream is not expandable exception  during array compression
        /// </summary>
        [TestMethod]
        [TestCategory("Unit")]
        [Priority(0)]
        public void AgentSerializableSmallArrayUncompressedTest()
        {
            double[] arrayToSerialize = new[] { 0.210d, 0.098d };
            SerializableArray array = new SerializableArray(false);
            array.PrepareToSerialize(arrayToSerialize);
            double[] deserialized = (double[])array.GetTypedArray();
            for (int i = 0; i < arrayToSerialize.Length; i++)
            {
                Assert.AreEqual(arrayToSerialize[i], deserialized[i]);
            }
        }

        /// <summary>
        /// Call default constructor and locator property
        /// </summary>
        [TestMethod]
        [Priority(0)]
        [TestCategory("Unit")]
        public void AgentDefaultConstructor()
        {
            CodeCoverageAgent agent = new CodeCoverageAgent();

            // The test passes if there is no exception
        }

        /// <summary>
        /// Test getting the locator property
        /// </summary>
        [Priority(0)]
        [TestMethod]
        [TestCategory("Unit")]
        public void AgentGetLocatorProperty()
        {
            CodeCoverageAgent agent = new CodeCoverageAgent("CodeCoverage");
            AgentLocator locator = new AgentLocator();
            IAgentHost host = locator.Register(agent);
            agent.TestGetLocator();

            // The test passes if there is no exception
        }

        /// <summary>
        /// Test getting the wrapped agent property
        /// </summary>
        [Priority(0)]
        [TestMethod]
        [TestCategory("Unit")]
        public void AgentGetWrapped()
        {
            CodeCoverageAgent agent = new CodeCoverageAgent("CodeCoverage");
            AgentLocator locator = new AgentLocator();
            IAgentHost host = locator.Register(agent);
            AgentHost agentHost = (AgentHost)host;

            Assert.IsNotNull(agentHost.WrappedAgent, "AgentHost Get wrapped Property is Null");
        }

        /// <summary>
        /// Test exercising the agent message properties.
        /// </summary>
        [Priority(0)]
        [TestMethod]
        [TestCategory("Unit")]
        public void AgentMessageTests()
        {
            AgentMessage message = new AgentMessage(AgentMessage.GetCurrentTime());
            long id = message.Id;
            long time = message.Timestamp;
            long seq = message.SequenceId;
            seq++;
            message.SequenceId = seq;
        }

        /// <summary>
        /// Verifies that agent messages contain the name of the agent that produced them.
        /// </summary>
        [Priority(0)]
        [TestMethod]
        [TestCategory("Unit")]
        public void AgentMessageOriginatingAgentName()
        {
            AgentLocator locator = new AgentLocator();
            int count = 10;

            var producer = new MessageInjectorAgent<SimpleAgentMessage<int>>("producer");
            IAgentHost producerHost = locator.Register(producer);
            var consumer = new MessageCollectorAgent<SimpleAgentMessage<int>>("consumer", producer, count);
            IAgentHost consumerHost = locator.Register(consumer);
            var controlReceiver = new VerifyNameInControlMessageAgent("ControlMessageReceiver");
            IAgentHost controlReceiverHost = locator.Register(controlReceiver);

            // start all agents
            producerHost.Activate();
            consumerHost.Activate();
            controlReceiverHost.Activate();

            // send messages from the producer to consumer
            int i = 0;
            while (!consumer.Wait(10))
            {
                for (int j = 0; j < 10; j++)
                {
                    producer.Publish(new SimpleAgentMessage<int>(i++, AgentMessage.GetCurrentTime()));
                }
            }

            // send a control message to the control reciever and check that the
            // originating agent name is set correctly in the message received by the agent
            IForwarder<SimpleAgentMessage<int>> controlPort = controlReceiverHost.GetControlPort<SimpleAgentMessage<int>>();
            controlPort.Post(new SimpleAgentMessage<int>(1, 0));
            Assert.AreEqual("ControlMessageReceiver", controlReceiver.GetOriginatingNameInControlMessage());

            // shutdown first, to ensure message flow is stopped and we can look at the resutls collection
            consumerHost.Deactivate();
            producerHost.Deactivate();
            controlReceiverHost.Deactivate();

            // all received messages should have "producer" as the originating agent name
            foreach (SimpleAgentMessage<int> m in consumer.Results)
            {
                Assert.AreEqual("producer", m.CreatingAgentName);
            }
        }

        /// <summary>
        /// Serializes and deserializes an array of the specified type with compression turned off,
        /// to validate the behavior of SerializableArray and SerializationSurrogate
        /// </summary>
        /// <typeparam name="T">The type of array elements to serialize</typeparam>
        /// <param name="elementSize">The size in bytes of the array element</param>
        /// <param name="value">The default value to assign to each element.</param>
        private void CheckUncompressedSerializableArray<T>(int elementSize, T value) where T : struct
        {
            int arraySize = 1024;
            int approxByteArraySize = 14000;
            int expectedXmlOverhead = 400;
            long expectedTotalSize = (elementSize * approxByteArraySize) + expectedXmlOverhead;
            this.CheckSerializableArray<T>(value, arraySize, expectedTotalSize, true);
        }

        /// <summary>
        /// Serializes and deserializes an array of the specified type with compression turned on,
        /// to validate the behavior of SerializableArray and SerializationSurrogate
        /// </summary>
        /// <typeparam name="T">The type of array elements to serialize</typeparam>
        /// <param name="elementSize">The size in bytes of the array element</param>
        /// <param name="value">The default value to assign to each element.</param>
        private void CheckCompressedSerializableArray<T>(int elementSize, T value) where T : struct
        {
            int arraySize = 1024;
            int approxByteArraySize = 1400;
            int expectedXmlOverhead = 400;
            long expectedTotalSize = (elementSize * approxByteArraySize) + expectedXmlOverhead;
            this.CheckSerializableArray<T>(value, arraySize, expectedTotalSize, false);
        }

        /// <summary>
        /// Checks serializable array serialization and deserialization
        /// </summary>
        /// <typeparam name="T">The primitive type of array</typeparam>
        /// <param name="value">The value to set each element of the array.</param>
        /// <param name="arraySize">The array size</param>
        /// <param name="expectedTotalSize">The expected total size of the serialized stream.</param>
        /// <param name="noCompression">Determines whether to compress the arrays before serialization or not.</param>        
        private void CheckSerializableArray<T>(T value, int arraySize, long expectedTotalSize, bool noCompression) where T : struct
        {
            DataContractSerializer serializer = new DataContractSerializer(
                typeof(SimpleAgentMessage<T[]>), null, 256, true, false, new SerializationSurrogate(noCompression));

            // create a message with an array of simple types
            SimpleAgentMessage<T[]> message = new SimpleAgentMessage<T[]>(new T[arraySize], 0);
            for (int i = 0; i < arraySize; i++)
            {
                message.State[i] = value;
            }

            using (MemoryStream stream = new MemoryStream())
            {
                // serialize the message
                serializer.WriteObject(stream, message);
                if (stream.Position > expectedTotalSize)
                {
                    throw new Exception("The serialized array was too big: " + stream.Position);
                }

                // now deserialize it
                stream.Seek(0, SeekOrigin.Begin);
                object result = serializer.ReadObject(stream);
                if (!(result is SimpleAgentMessage<T[]>))
                {
                    throw new Exception("The deserialized message was of the wrong type: " + result.GetType().FullName);
                }

                SimpleAgentMessage<T[]> resultMessage = result as SimpleAgentMessage<T[]>;
                if (resultMessage.State.Length != arraySize)
                {
                    throw new Exception("The deserialized array has the wrong size: " + resultMessage.State.Length);
                }

                for (int i = 0; i < arraySize; i++)
                {
                    if (!resultMessage.State[i].Equals(value))
                    {
                        throw new Exception(
                            "The deserialized array has the wrong element value: " + resultMessage.State[i]);
                    }
                }
            }
        }
    }
}
